import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeoutException;
import java.util.*;


public class Helpers {	

	public static int SERVERNAME = 0;
	public static int IPADDRESS = 1;
	public static int PORTNUMBER = 2;
	public static int PARENTMSD = 3;
	public static int HELLO_NUMCHILDREN = 3;
	public static int HELLO_IDHASH = 4;
	public static int ADDSPACE_RANGESTART = 3;
	public static int ADDSPACE_RANGEEND = 4;
	public static int REPHELLO_RANGESTART = 4;
	public static int REPHELLO_RANGEEND = 5;
	public static int CHECK_KEYWORD = 4;
	public static int REGISTER_KEYWORD = 4;
	public static int MSDPROBE_KEYWORD = 4;
	public static int MSDPROBE_HOPCOUNT = 7;
	public static int MSDPROBE_MSDIP = 5;
	public static int MSDPROBE_MSDPORT = 6;
	public static int MSDPROBEREPLY_MSDIP = 5;
	public static int MSDPROBEREPLY_MSDPORT = 6;
	public static int MSDPROBEREPLY_HOPCOUNT = 7;
	public static int EXTSEARCH_PARAMETER = 4;
	public static int INVALIDATE_KEYWORD = 4;
	public static int QUERY_KEYWORD = 4;
	public static int REDIRECT_KEYWORD = 5;
	public static int REDIRECT_HOST = 6;
	public static int REDIRECT_PORT = 7;
	public static int REDIRECT_FLAG = 4;
	public static int SEARCH_PARAMETER = 4;
	public static int SEARCHRESPONSE_KEYWORD = 5;
	public static int SEARCHRESPONSE_NAME = 6;
	public static int SEARCHRESPONSE_DETAILS = 6;
	public static int EXTSEARCHINVALID_KEYWORD = 4;
	public static int EXTSEARCHRESPONSE_DETAILS = 6;
	public static int EXTSEARCHRESPONSE_NAME = 6;
	public static int EXTSEARCHRESPONSE_KEYWORD = 5;
	public static int CHECKRESPONSE_TORF = 3;
	public static int REGISTERSTATUS_TORF = 3;
	public static int QUERYRESPONSE_NUMFIELDS = 2;
	public static int QUERYRESPONSE_DETAILS = 4;
	public static int TX_END_KEYWORD = 4;
	
	public static void waitForAllThreadsToClose(Thread[] threads, int timeoutSeconds) 
		throws TimeoutException
	{
		Calendar now = Calendar.getInstance();
		long start = now.getTimeInMillis();
		long timeout = start + timeoutSeconds*1000;
		
		boolean dead = false;
	    while(!dead)
	    {
	    	dead = true;
	    	for(Thread t : threads)
	    	{
	    		if(t != null)
	    			dead &= !t.isAlive();
	    	}
	    	now = Calendar.getInstance();
	    	if(now.getTimeInMillis() > timeout)
	    	{
	    		throw new TimeoutException(
	    				"Timed out waiting for all threads to close.");
	    	}
	    }		
	}

	public static boolean isParent(String[][] ServerConfigInfo, String[] server) {
		//parents are servers whose names (first col) appear in some other
		//servers parent column
		boolean isParent = false;
		//TODO:
		return isParent;
	}

	public static boolean isChild(String[] server) {
		//children are servers whose parents are not "root"
		boolean isChild = false;
		//TODO:
		return isChild;
	}
	
	public static MSDInfo isInChildHashRange(ArrayList<MSDInfo> childList, String hashedKeyword){
		for(MSDInfo info: childList)
			if(info.hashRange.isInRange(hashedKeyword))
				return info;
		return null;
	}
	
	public static boolean isRoot(String[] server) {
		return !isChild(server);
	}

	public static int getNumAllChildren(String[][] serverConfigInfo, String[] server)
	{
		int count = 0;
		if(server == null)
			return 0;
		String[][] children = getChildren(serverConfigInfo, server);
		if(children == null)
			return 0;
		count = children.length; //for child encountered
		for(String[] child: children)
		{
			count += getNumAllChildren(serverConfigInfo, child);
		}
		return count;
	}
	
	public static String[][] getChildren(String[][] serverConfigInfo, String[] server)
	{
		Queue<String[]> childrenQueue = new LinkedList<String[]>();
		String[][] children = new String[childrenQueue.size()][];
		//TODO:
		return children;
	}
	
	public static String[] getParent(String[][] serverConfigInfo, String[] server)
	{
		//TODO:
		return null;
	}
	
	public static String getMD5Hash(String pass) {
		MessageDigest m;
		try {
			m = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			return null;
		}
		byte[] data = pass.getBytes(); 
		m.update(data,0,data.length);
		BigInteger i = new BigInteger(1,m.digest());
		return String.format("%1$032X", i);
	}

	public static String[][] getHashBoundaries(String addressStartHex32,
			String addressEndHex32, int numSpaces) 
	{
		BigInteger numSpacesBI = new BigInteger(Integer.toString(numSpaces));
		BigInteger addressStartBI = new BigInteger(addressStartHex32, 16);
		BigInteger addressEndBI = new BigInteger(addressEndHex32, 16);
		BigInteger width = addressEndBI.subtract(addressStartBI);
		BigInteger nodeWidth = width.divide(numSpacesBI);
		BigInteger remainder = width.mod(numSpacesBI);
		  
		String[][] keywordHashBoundaries = new String[numSpaces][2];
		BigInteger start = addressStartBI;
		BigInteger end;
		for(int i = 0; i < numSpaces; i++)
		{
			BigInteger iBI = new BigInteger(Integer.toString(i));
			end = start.add(nodeWidth);
			if(i == numSpaces - 1)
				end = addressEndBI;
			keywordHashBoundaries[i][0] = String.format("%1$032X", start);
			keywordHashBoundaries[i][1] = String.format("%1$032X", end);
			start = end.add(BigInteger.ONE);
		}
		
		return keywordHashBoundaries;
	}

	public static void sendMessage(ConnectionDetails connDetails, String message){
		String[] messages = new String[1];
		messages[0] = message;
		sendMessage(connDetails, messages);
	}
	
	public static void sendMessage(ConnectionDetails connDetails, String[] messages){
		Socket socket = null;
        PrintWriter out = null;
        
        try{
        	socket = new Socket(connDetails.hostNameOrIP, connDetails.port);
        	out = new PrintWriter(socket.getOutputStream(), true);
        } catch (UnknownHostException e){
        	e.printStackTrace();
        } catch(IOException e){
        	e.printStackTrace();
        } catch(Exception e){
        	e.printStackTrace();
        }
        
        for(String message: messages){
        	System.out.println("  Helpers.sendMessage: --> " + connDetails.hostNameOrIP + ": " + message);
        	System.out.flush();
        	out.println(message);	
        }
        
		out.close();
		try{
			socket.close();
		} catch (UnknownHostException e){
        	e.printStackTrace();
        } catch(IOException e){
        	e.printStackTrace();
        } catch(Exception e){
        	e.printStackTrace();
        }
	}
	
	public static String sendMessageRcvReply(ConnectionDetails connDetails, String message) {
		//throws UnknownHostException, IOException
		Socket socket = null;
	    PrintWriter out = null;
	    BufferedReader in = null;
	    
	    try{
		    socket = new Socket(connDetails.hostNameOrIP, connDetails.port);
		    socket.setSoTimeout(10000);
		    out = new PrintWriter(socket.getOutputStream(), true);
		    in = new BufferedReader(new InputStreamReader(
		                                socket.getInputStream()));
	    } catch (UnknownHostException e){
        	e.printStackTrace();
        } catch(IOException e){
        	e.printStackTrace();
        } catch(Exception e){
        	e.printStackTrace();
        }
	    System.out.println("  Helpers.sendMessageRcvReply: --> " + connDetails.hostNameOrIP + ": " + message);
	    System.out.flush();
	    out.println(message);
	    String reply = null;
	    try{
	    	reply = in.readLine();	//should timeout because the socket.setSoTimeout is set above
	    }
	    catch(SocketTimeoutException e){
	    	e.printStackTrace();
	    } catch (UnknownHostException e){
        	e.printStackTrace();
        } catch(IOException e){
        	e.printStackTrace();
        } catch(Exception e){
        	e.printStackTrace();
        }
	    if(reply != null)
	    	System.out.println("  Helpers.sendMessageRcvReply: <-- " + connDetails.hostNameOrIP + ": " + reply);
	    else
	    	System.out.println("  Helpers.sendMessageRcvReply: <-- " + connDetails.hostNameOrIP + ": Timed out before response");
	    System.out.flush();
	    
	    //out.println("bye 0x0B 0");
	    
		out.close();
		try{
			in.close();
			socket.close();
		} catch (UnknownHostException e){
        	e.printStackTrace();
        } catch(IOException e){
        	e.printStackTrace();
        } catch(Exception e){
        	e.printStackTrace();
        }
		return reply;	//reply will be null if a timeout occurred
	}
	
	public static String[] extractFields(String line, String delimeter, int startIndex)
	{
		String[] fields = line.split(delimeter);
		Queue<String> nonEmptyFieldsQueue = new LinkedList<String>();
		
		for (int i=0; i < fields.length; i++)
			if(fields[i].trim().length() > 0)
				nonEmptyFieldsQueue.add(fields[i].trim()); //trim any whitespace from each field
		
		String[] nonEmptyFields = new String[nonEmptyFieldsQueue.size()];
		int index = 0;
		int nonEmptyFieldsIndex = 0;
		while(nonEmptyFieldsQueue.size() > 0)
		{
			String val = nonEmptyFieldsQueue.remove();
			if(index >= startIndex)
			{
				nonEmptyFields[nonEmptyFieldsIndex] = val;
				nonEmptyFieldsIndex++;
			}
			index++;
		}
		return nonEmptyFields;
	}
	
	public static String[] extractFields(String line, String delimeter)
	{
		return extractFields(line, delimeter, 0);
	}
	
	public static String arrayToDelimitedList(String[] input, String delimeter)
	{
		if(input==null)
			return null;
		String result = "";
		for(int i=0; i<input.length; i++)
		{
			if(input[i] == null || input[i].trim().length() == 0)
				continue;
			result += input[i];
			if(i<input.length-1)
				result += delimeter;
		}
		return result;
	}

	public static MSDInfo findChildByConnectionDetails(
			ArrayList<MSDInfo> childInfoList, ConnectionDetails connectionDetails)
	{
		 for (MSDInfo child : childInfoList)
		 {
			 if(child.connectionDetails.equals(connectionDetails))
				 return child;
		 }
		 return null;
	}

	public static int getChildCounts(ArrayList<MSDInfo> childInfoList) {
		int sum = 0;
		for (MSDInfo child : childInfoList)
		 {
			sum += child.domainCount;
		 }
		 return sum;
	}
}
